Some Structural Design Patterns
00 - Table of Contents
01 - Introduction
02 - Composite
03 - Decorator
04 - Proxy
05 - Other Structural Patterns
01 - Introduction
In the good old times of non Object Oriented applications, code and data were two different things. People used to write routines to manipulate given data structures while in the OO world, we just write operations on classes that represents an abstract view of the stored data of the data structure itself.
The new concepts introduced by OOP added a new challenge to software design: coders would have now to cope with complex structures of class hierarchies.
Just like records in a database, your objects are linked to other objects using references and/or pointers.
02 - Composite
Before we start, you have to know that this pattern is one of my favorite! =)
In demos, we often have to deal with hierarchical structures and that's exactly
what the Composite Pattern is about. The composite allows any client
application to treat the same way both hierarchy components and parts of the
hierarchy itself.
--------- |Component|--------------------------+ --------- | | | inherits . . . . . | contains . . . .| | | /_\ | +-----------------------------------------+ | | | | | | ---------- ---------- ---------- --------- | |Component1| |Component2| |ComponentN| |Composite|<>---+ ---------- ---------- ---------- ---------The Composite class holds the references to all the components stored into it.
Let's say the context is a Ray-Tracing system and we'd like to be able to
apply geometrical transforms to the objects as much as t.
class Component { public: virtual void apply_transform( Matrix *m ) = 0; }; class Composite : public Component { vectorOf course, the problem is simplified here since all the primitives should actually inherit from a Model interface that defines the operations required by the rendering engine as such as finding the hit distance for a given ray, calculating the color of the hit point and so on.members; public: void add( Component *c ); virtual void apply_transform( Matrix *m ); }; class Plane : public Component { public: // ... virtual void apply_transform( Matrix *m ); }; class Sphere : public Component { public: // ... virtual void apply_transform( Matrix *m ); };
In the primitive objects (Plane and Sphere here), the result of this method will be some vector arithmetic operation on the internal representation of the 3-dimensional model.
Now, let's take a closer look on how to implement the composite object. What
we have to do when the method apply_transform() is called is to call that
method on all the object referenced in this composite.
void Composite::add( Component *c ) { members.push_back( c ); } void Composite::apply_transform( Matrix *m ) { for ( int i = 0; i < members.size(); i++ ) members[i]->apply_transform( m ); }If this method is called on another composite object, the transformation will naturally bo propagated to contained objects.
This pattern is heavily used in the Java Abstract Windowing Toolkit GUI framework.
Btw, did you already think about the screen being a container of effects? Imagine having a "root" effect (whose action would be to clear the screen or draw some background bitmap or effect) and then you just add several layers of effects (first layer 3D objects, second layer,...). With such a class architecture, the mainloop is the same for the whole demo, only the contents of the "root" container changes... just take a couple of minutes to think about it, you all scripted demo addicts! =)
03 - Decorator
This Pattern is used to add responsibilities to an object in a dynamic and flexible way.
Remember this: "A decorator changes the skin of an object, not its guts."
Basically, a Decorator is a container of Components as such as the Composite
Pattern we've studied previously. The Decorator is in fact a degenerated form
of Composite patterns with only one type of concrete Component.
--------- |Component| |---------|-------------+ | foo() | | --------- | inherits . . . . . | | /_\ |. . . contains +---------------+ | | | | ---------- --------- | |AComponent| |Decorator|<>---+ ---------- --------- | /_\ +--------------------------+ | | | ---------- ---------- ---------- |Decorator1| |Decorator2| |DecoratorN| ---------- ---------- ----------The DecoratorX are actually Strategy Patterns (see Behavioural Patterns).
The Pattern Decorator allows you to easily add (or remove) responsibility from an object. It's also useful when you need to extend the behaviour of a class and when the inheritance is not available (too many sub-classes or non virtual methods in the super-class).
Check out the following instance chart:
---------- |Decorator1| |----------| ---------- |component-+------>|Decorator2| ---------- |----------| ---------- |component-+------>|AComponent| ---------- ----------You have to notice that the final component doesn't hold any reference to its decorators. So if you had any other decorators, you don't have to change a bit in the implementation of the class AComponent.
The Java I/O stream API is a good example of Decorators. First you get some
sort of raw stream, you decorate it with a buffered stream to increase
performance and you finally decorate it with a higher level data access
strategy to consider the streams as lines of text or as fixed block records
or whatever. So we have something like (class names are pure fiction):
HigherLevelReader hlr = new BufferedReader( new StreamReader( something.getStream() ) ); customBlock = hlr.readCustomBlockOrSomething();You just customize (decorate) the objects by adding new features between the original component (a stream here) and the end user.
04 - Proxy
I'm sure that lots of you are familiar with the concept of Proxy in the world of the Internet. The Proxy Pattern has much the same function as the Proxy server of your ISP. Its goal is to be a surrogate to another object in order to check the access to this object.
So we have something like the following instance diagram:
------ ----- ------ |Client|------>|Proxy|------->|Object| ------ ----- ------There's no point writing a Proxy that does a 1-to-1 mapping between its methods and the methods of the "hidden" object. The Proxy should be a "value-added" object. The Proxy can:
- perform some pre or post-processing before/after calling the service;
- check the arguments before passing them to the real object;
- operate access control on the resource (like handling semaphores);
- hide technical details (distributed objects);
- cache data to avoid to call the service on the "hidden" object
- ...
When writing Proxies in C++, it might be useful to think about overloading the dereference operators : * and ->. This will give a real transparent access to the object from the client application.
Let's imagine you have to render a very large interactive 3D scene which use
lots of different 256x256 32-bpp textures. Since you are a nice coder, you
won't oblige people to have 256 megs of memory to enjoy your smashing
production and you'll only keep unpacked the texture actually needed to
rasterize the current image and others will be left compressed in memory.
------------ |Texture_Base| ------------ inherits . . . . . . | /_\ +-----------------+ | | ------------- ------- |Texture_Proxy|<>----|Texture| ------------- . ------- . contains oneAs you can see, with this common interface, we can use Proxies as if they were the real object (the class Texture here).
And now, let's examine some tasty chunks of source code for all this:
// Texture.h class Texture_Base { // ... public: Texture_Base( char *filename ); virtual ~Texture_Base(); // ... virtual unsigned *pixelmap() const = 0 }; class Texture : public Texture_Base() { public: // ... // these method is specific to this class void unpack_data(); bool is_unpacked() const; // Texture implements pixelmap; unsigned *pixelmap() const; }; class Texture_Proxy : public Texture_Base { Texture *text; public: Texture_Proxy( char *filename ); // ... unsigned *pixelmap() const; }; // Texture.cc // We take a closer look at the implementation // of the Proxy object Texture_Proxy::Texture_Proxy( char *filename ) { text = new Texture( filename ); } // unpack data whenever required unsigned *Texture_Proxy::pixelmap() const { if ( text->is_unpacked() ) text->unpack_data(); return text->pixelmap(); }Of course, this is not the most efficient way to handle this problem - using a texture cache is obviously better and we didn't took care here about the way useless unpacked data is flushed - but this was only an example and I'm sure that you'll find lots of other exciting applications for this Pattern!
05 - Other Structural Patterns
There are much more Structural Patterns but I'm kinda busy for the moment and I miss time to cover them in this document. Drop me email if you want to see other Patterns developed in your favorite column!
For your information, here's a brief description of some other Structural Patterns:
- Adapter, the interface of an object
- Bridge, the implementation of an object
- Facade, the interface of a sub-system
- Flyweight, the storage cost of objects